package com.gc.cscott.cli;

import com.gc.cscott.domain.Competitor;
import com.gc.cscott.domain.Executable;
import com.gc.cscott.domain.Solution;
import com.gc.cscott.domain.contest.Contest;
import com.gc.cscott.domain.contest.Source;
import com.gc.cscott.domain.contest.Task;
import com.gc.cscott.domain.contest.TestCase;
import com.gc.cscott.domain.contest.numbers.TaskNumber;
import com.gc.cscott.domain.contest.numbers.TestCaseNumber;
import com.gc.cscott.domain.result.TestCaseResult;
import com.gc.cscott.fs.FileSystemWorker;
import com.gc.cscott.run.Chaser;
import com.gc.cscott.run.Executor;
import com.gc.cscott.store.ResultsStorage;
import com.gc.cscott.util.CscottFomatter;
import com.gc.cscott.util.CscottUtil;
import com.gc.cscott.verify.OutputVerifier;
import com.gc.cscott.verify.OutputVerifier.VerifiedOutput;
import com.gc.cscott.verify.OutputVerifierFactory;
import com.google.common.collect.ImmutableList;
import com.google.inject.Inject;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
import java.util.logging.FileHandler;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.apache.commons.io.FileUtils;

/**
 *
 * @author root
 */
public class PerformTestsCommand implements Command {
    private static final Logger log = Logger.getLogger(PerformTestsCommand.class.getSimpleName());

    @Inject
    Contest contest;

    @Inject
    FileSystemWorker fsWorker;

    @Inject
    Executor executor;

    @Inject
    Chaser chaser;

    @Inject
    OutputVerifierFactory outputVerifierFactory;

    @Inject
    ResultsStorage resultsStorage;

    ResultsStorage.WriteMode writeMode = ResultsStorage.WriteMode.Append;
    boolean noCheck = false;

    public List<String> listHelp() {
        return ImmutableList.of(
                "Will perform test on specified solution.",
                "Extra arguments:",
                "  S1 - number of solutions folders in config (e.g. S1, S2, etc.),",
                "       all solutions will be tested if omitted.",
                "  T1 - tasks ordinal nambers to test,",
                "       all tasks will be testes if omitted. ",
                "  t1,2-3 - number of tests to perform.",
                "       works only if SINGLE task is specified.",
                "       All tests will be runned if omitted.",
                "  'overwrite' - will overwrite current result",
                "       if it is presented. By default if result",
                "       is presented solution will not be retested.",
                "  'nocheck' - only run solutions, do not verify");
    }

    public List<String> perform(List<String> pendingArgs) {
        Set<Integer> solutionsToTest = new TreeSet<Integer>();
        Set<TaskNumber> tasksToRun = new TreeSet<TaskNumber>();
        Set<TestCaseNumber> testCasesToRun = new TreeSet<TestCaseNumber>();

        ImmutableList.Builder<String> notProcessedArgumentsListBuilder = ImmutableList.<String>builder();
        boolean processingArguments=true;
        for (String argument : pendingArgs) {
            if (processingArguments==true){
                { //solution.
                    List<Integer> solutionsList = Parser.parseSolutionList(argument);
                    if (solutionsList!=null){
                        solutionsToTest.addAll(solutionsList);
                        continue;
                    }
                }
                { //task.
                    List<TaskNumber> tasksList = Parser.parseTaskList(argument);
                    if (tasksList!=null){
                        tasksToRun.addAll(tasksList);
                        continue;
                    }
                }
                { //test
                    List<TestCaseNumber> testCaseList = Parser.parseTestCaseList(argument);
                    if (testCaseList!=null){
                        testCasesToRun.addAll(testCaseList);
                        continue;
                    }
                }
                { //overwrite and no check.
                    if ("overwrite".equals(argument)==true){
                        writeMode = ResultsStorage.WriteMode.Owerwrite;
                        continue;
                    }
                    if ("nocheck".equals(argument)==true){
                        noCheck = true;
                        continue;
                    }
                }

                if (argument.startsWith("-")==false){
                    throw new Error("Unknown argument: " + argument+" in pending arguments list: "+pendingArgs);
                } else {
                    notProcessedArgumentsListBuilder.add(argument);
                    processingArguments = false;
                }
            } else {
                notProcessedArgumentsListBuilder.add(argument);
            }
        }
        { //check solutions.
            final int maxSolutionNumber = contest.getSourcesList().size();
            if (solutionsToTest.isEmpty() == true) {
                solutionsToTest.addAll(fromOneToNumber(maxSolutionNumber));
            } else {
                for (Integer solutionNumber : solutionsToTest) {
                    if (checkRangeFromOneToLimint(solutionNumber, maxSolutionNumber) == false) {
                        throw new Error("Solution number " + solutionNumber + " is out of range [1.." + maxSolutionNumber + "]");
                    }
                }
            }
        }
        {//check tasks.
            if (tasksToRun.isEmpty() == true) {
                tasksToRun.addAll(contest.getTaskNumbersAvailable());
            } else {
                for (TaskNumber taskNumber : tasksToRun) {
                    if (contest.getTask(taskNumber) == null) {
                        throw new Error("Task number " + taskNumber + " is not in list " + contest.getTaskNumbersAvailable());
                    }
                }
            }

        }
        if (testCasesToRun.size() > 0 && tasksToRun.size() != 1) {
            throw new Error("Single task must be specified to run particular  tests.\n"
                    + "But specified task list: " + tasksToRun + "\n"
                    + "and tests expected to run: " + testCasesToRun);
        } else {
            //TODO validate test numbers.
        }

        log.info("Prepared to test:");
        log.log(Level.INFO, "Solutions: {0}", solutionsToTest);
        log.log(Level.INFO, "Tasks: {0}", tasksToRun);
        log.log(Level.INFO, "Test: {0}", (testCasesToRun.isEmpty() ? "all" : testCasesToRun));

        testSolutions(solutionsToTest, tasksToRun, testCasesToRun);
        return notProcessedArgumentsListBuilder.build();
    }

    private boolean checkRangeFromOneToLimint(int value, int limit) {
        return 1 <= value && value <= limit;
    }

    private Set<Integer> fromOneToNumber(int number) {
        Set<Integer> resultSet = new TreeSet<Integer>();
        for (int i = 1; i <= number; i++) {
            resultSet.add(i);
        }
        return resultSet;
    }

    private void testSolutions(Set<Integer> solutionsToTest, Set<TaskNumber> tasksToRun, Set<TestCaseNumber> testsToRun) {
        for (Integer solutionNumber : solutionsToTest) {
            Source source = contest.getSourcesList().get(solutionNumber - 1);
            if (source.isEnabled()==false) {
                log.log(Level.INFO, "Skipping solutions from source: {0}", source.getLocation());
            } else {
                log.log(Level.INFO, "Testing solutions from source: {0}", source.getLocation());
                testCompetitorsInDerectory(source.getLocationDir(), tasksToRun, testsToRun);
            }
        }
    }

    private void testCompetitorsInDerectory(File sourceDir, Set<TaskNumber> tasksToRun, Set<TestCaseNumber> testsToRun) {
        boolean printed = false;
        for (Competitor competitor : fsWorker.listCompetitors(sourceDir)) {
            log.log(Level.INFO, "  Testing competitor: ''{0}'' - {1}", new Object[]{competitor.getName(), competitor.getBasedir().getAbsolutePath()});
            testSoleCompetitor(competitor, tasksToRun, testsToRun);
            printed = true;
        }
        if (printed == false) {
            log.info("    No competitors in directory");
        }
    }

    private FileHandler createPersonalLogHandler(Competitor competitor){
        final FileHandler fileHandler;
        try {
            fileHandler = new FileHandler(competitor.getBasedir() + "/cscott-" + CscottUtil.currentTimeString() + "-all.log", true);
        } catch (Exception ex) {
            throw new Error("Failed to configure personal logger for competitor: "+competitor.getName(), ex);
        }
        fileHandler.setFormatter(new CscottFomatter());
        fileHandler.setLevel(Level.ALL);
        return fileHandler;
    }

    private void testSoleCompetitor(Competitor competitor, Set<TaskNumber> tasksToRun, Set<TestCaseNumber> testsToRun) {
        //prepare competitor's personal logger.
        final Logger global = Logger.getLogger("");
        final FileHandler personalHandler = createPersonalLogHandler(competitor);
        global.addHandler(personalHandler);

        try {
            List<Solution> solutionsList = fsWorker.listSolutions(competitor);
            for (TaskNumber taskNumber : tasksToRun) {
                Task task = contest.getTask(taskNumber);
                boolean tested = false;
                for (Solution solution : solutionsList) {
                    if (solution.getTaskNumber() == taskNumber.intValue()) {
                        log.log(Level.INFO, "    Testing solution No: {0} ({1})", new Object[]{solution.getTaskNumber(), competitor.getName()});
                        testSoleSolution(competitor, solution, task, testsToRun);
                        tested = true;
                    }
                }
                if (tested == false) {
                    log.log(Level.INFO, "    No solution for task: {0} ({1})", new Object[]{taskNumber, competitor.getName()});
                }
            }
        } finally {
            //remove competitor's personal logger.
            global.removeHandler(personalHandler);
            personalHandler.close();
        }
    }

    private void testSoleSolution(Competitor competitor, Solution solution, Task task, Set<TestCaseNumber> testsToRun) {
        for (TestCaseNumber testCaseNumber : task.getTestCaseNumbersAvailable()) {
            if (testsToRun.isEmpty() == false && testsToRun.contains(testCaseNumber) == false) {
                continue; //skip test.
            } else {
                log.log(Level.INFO, "      Running task {1} test {0} ({2})", new Object[]{testCaseNumber, task.getNumber().intValue(), competitor.getName()});
                testSoleSolutionOnSoleTest(competitor, solution, task, task.getTestCase(testCaseNumber));
            }
        }
    }

    private void testSoleSolutionOnSoleTest(
            final Competitor competitor,
            final Solution solution,
            final Task task,
            final TestCase testCase) {
        File workingDir = null;
        try {
            if (true) { //we need this "if" to be able to write throw Runtume in the end.
             try {
                    if (resultsStorage.hasResults(competitor, testCase) == true) {
                        if (writeMode.isSkipTestWhenPresended()==true){
                            log.log(Level.INFO, "        Already tested, skipping.");
                            return;
                        } else {
                            log.log(Level.INFO, "        Overwriting result.");
                        }
                    }
                } catch (IOException ex) {
                    throw new Error("Failed to check current result state", ex);
                }
                
                try {
                    workingDir = fsWorker.createWorkingDirectory(new File(contest.getWorkDir()));
                } catch (IOException ex) {
                    throw new Error("Failed to create working directory", ex);
                }

                try {
                    fsWorker.copyTestCaseInputIntoWorkingDirectory(testCase, workingDir);
                } catch (IOException ex) {
                    throw new Error(ex);
                }
                try {
                    Executable executable = fsWorker.findExecutableInSolution(solution);
                    if (executable.getPlatform().isCopyBeforeRun()==true){
                        File newExuecutableFilePath = new File(workingDir,executable.getExecutableFilePath().getName());
                        try {
                            FileUtils.copyFile(executable.getExecutableFilePath(), newExuecutableFilePath);
                        } catch (IOException ex) {
                            throw new Error("Failed to copy file before execute.",ex);
                        }
                        executable = new Executable(newExuecutableFilePath, executable.getPlatform());
                        log.log(Level.INFO, "        New target executable: {0}", executable.getExecutableFilePath().getAbsolutePath());
                    }
                    try {
                        Process process = executor.runExecutable(executable, executable.getPlatform().getRun(), workingDir);
                        final Chaser.ChaseResult chaseResult = chaser.chase(process, testCase.getTimeout());
                        log.log(Level.FINE, "        path: {0}", executable.getExecutableFilePath().getAbsolutePath());
                        log.log(Level.FINE, "        platform: {0}", executable.getPlatform().getName());
                        log.log(Level.FINE, "        exit code: {0}", chaseResult.getExitCode());
                        log.log(Level.FINE, "        time: {0}", chaseResult.getExecutionTime());
                        log.log(Level.FINE, "        timeout: {0}", chaseResult.isTimeout());
                        if (chaseResult.getStdout().length()>1024*1024){
                            log.log(Level.FINE, "        TOOO LONG STDOUT TO LOG");
                        } else {
                            log.log(Level.FINE, "        stdout:\n{0}EOF", chaseResult.getStdout());
                        }
                        log.log(Level.FINE, "        stderr:\n{0}EOF", chaseResult.getStderr());
                        if (chaseResult.isTimeout() == true) {
                            log.log(Level.INFO, "        FAILED (timeout)");
                            storeResult(competitor, new TestCaseResult() {
                                {
                                    setHint(null);
                                    setPayedTime(chaseResult.getExecutionTime());
                                    setResult(HowEnded.Timeout);
                                    setTestCase(testCase);
                                }
                            });
                            try {
                                Thread.sleep(50); //in case of timeout we should wait for a while to let OS make all aquired by executed application files free.
                            } catch (InterruptedException ex) {
                                throw new Error(ex);
                            }
                            return;
                        } else {
                            File solutionOutputFile = fsWorker.getSolutionOutputFile(task, workingDir);
                            if (solutionOutputFile.isFile() == false) {
                                for (File file : workingDir.listFiles()) {
                                    if (file.getName().equals(task.getInputFile()) == false) {
                                        log.log(Level.SEVERE, "        Possibly wrong output file name: {0}", file.getName());
                                    }
                                }
                                log.log(Level.INFO, "        FAILED (no output)");
                                storeResult(competitor, new TestCaseResult() {
                                    {
                                        setHint(null);
                                        setPayedTime(chaseResult.getExecutionTime());
                                        setResult(HowEnded.NoOutputFile);
                                        setTestCase(testCase);
                                    }
                                });
                                return;
                            } else {
                                String outputText;
                                try {
                                    outputText = FileUtils.readFileToString(solutionOutputFile, "ASCII");
                                } catch (IOException ex) {
                                    throw new Error("Failed to load solution output file", ex);
                                }
                                final String outputDumpName = storeOutput(competitor, testCase, outputText);
                                if (noCheck) {
                                    storeResult(competitor, new TestCaseResult() {{
                                            setHint("Not yet verified");
                                            setPayedTime(chaseResult.getExecutionTime());
                                            setResult(HowEnded.NotVerified);
                                            setTestCase(testCase);
                                            setOutputDumpName(outputDumpName);
                                        }
                                    });
                                } else {
                                    File validationFile = fsWorker.getTestCaseValidationFile(testCase);
                                    final String validationText;
                                    try {
                                        validationText = FileUtils.readFileToString(validationFile, "ASCII");
                                    } catch (IOException ex) {
                                        throw new Error("Failed to load validation file", ex);
                                    }
                                    final String inputText;
                                    try {
                                        inputText = FileUtils.readFileToString(fsWorker.getTestCaseInputSourceFile(testCase),"ASCII");
                                    } catch(IOException ex){
                                        throw new Error("Failed to load input file", ex);
                                    }
                                    OutputVerifier outputVerifier = outputVerifierFactory.getInstance(task.getNumber());
                                    final VerifiedOutput verifiedOutput = outputVerifier.verifyOuput(task.getNumber(), inputText, outputText, validationText);
                                    storeResult(competitor, new TestCaseResult() {{
                                            setHint(verifiedOutput.getDetails());
                                            setPayedTime(chaseResult.getExecutionTime());
                                            if (verifiedOutput.getOutputStatus() == OutputVerifier.OutputStatus.Correct) {
                                                setResult(HowEnded.Correct);
                                                log.info("        PASSED");
                                            } else {
                                                setResult(HowEnded.Wrong);
                                                log.log(Level.INFO, "        FAILED ({0})", verifiedOutput.getDetails());
                                            }
                                            setTestCase(testCase);
                                            setOutputDumpName(outputDumpName);
                                        }
                                    });
                                }
                                return;
                            }
                        }
                    } catch (final IOException ex) {
                        log.log(Level.INFO, "        FAILED (can not start executable) - {0}", ex.getMessage());
                        storeResult(competitor, new TestCaseResult() {
                            {
                                setHint(ex.getMessage());
                                setPayedTime(0L);
                                setResult(HowEnded.CanNotExecute);
                                setTestCase(testCase);
                            }
                        });
                        return;
                    }
                } catch (final FileNotFoundException ex) {
                    log.log(Level.INFO, "        FAILED (can not find executable) - {0}", ex.getMessage());
                    storeResult(competitor, new TestCaseResult() {
                        {
                            setHint(ex.getMessage());
                            setPayedTime(0L);
                            setResult(HowEnded.ExecutableNotFound);
                            setTestCase(testCase);
                        }
                    });
                    return;
                }
            }
            throw new RuntimeException("This line should never be reached");
        } finally {
            //will erasy working directory only if nothing fatal is happened.
            if (workingDir != null) {
                fsWorker.eraseWorkingDirectory(workingDir);
            }
        }
    }

    private void storeResult(Competitor competitor, TestCaseResult testCaseResult) {
        try {
            resultsStorage.storeResult(competitor, testCaseResult,writeMode);
        } catch (IOException ex) {
            throw new Error("Failed to store results", ex);
        }
    }

    private String storeOutput(Competitor competitor, TestCase testCase, String solutionOutputText) {
        try {
            return resultsStorage.storeOutput(competitor, testCase, solutionOutputText,writeMode);
        } catch (IOException ex) {
            throw new Error("Failed to store results", ex);
        }
    }
}
